跳到主要内容

Frame、Window、网络请求、存储

7 Frame 和 windw

7.1 弹窗和 window 的方法

弹窗(popup)就是打开一个给定 URL 的新窗口。大多数浏览器都配置为在新选项卡中打开 URL,而不是一个单独的窗口。

window.open("https://www.xxx.com/")

目前,进行OAuth 授权(使用 Google/Facebook/ ...登录),仍要使用弹窗,因为:

  1. 弹窗是一个独立的窗口,有自己独立的 JavaScript 环境。因此为了安全起见,常用弹窗方式打开一个不信任的第三方网站。
  2. 打开弹窗的代码很容易实现。

7.1.1 阻止弹窗

如果弹窗是在用户触发的事件处理程序(如 onclick之外调用,大多数浏览器都会默认阻止此类弹窗。

window.open('https://javascript.info');     // 弹窗被阻止

button.onclick = () => { // 弹窗被允许
window.open('https://javascript.info');
};

7.1.2 打开弹窗

window.open(url, name, params)

  • url:要在新窗口中加载的 URL。
  • name:新窗口的名称。每个窗口都有一个 window.name,指定哪个窗口用于打开URL网页。
    • 如果已经有 name 名字的窗口,将在该窗口打开给定的 URL;否则会打开一个新窗口。
  • params:新窗口的配置字符串。它包括设置,用逗号分隔。参数之间不能有空格,
    • 例如:width=200,height=100

params 的设置项:

  • 位置:
    • left/top(数字):屏幕上窗口的左上角的坐标。这有一个限制:不能将新窗口置于屏幕外(offscreen)。
    • width/height(数字):新窗口的宽度和高度。宽度/高度的最小值是有限制的,因此不可能创建一个不可见的窗口。
  • 窗口功能:
    • menubar(yes/no):显示或隐藏新窗口的浏览器菜单。
    • toolbar(yes/no):显示或隐藏新窗口的浏览器导航栏(后退,前进,重新加载等)。
    • location(yes/no):显示或隐藏新窗口的 URL 字段。Firefox 和 IE 浏览器不允许默认隐藏它。
    • status(yes/no):显示或隐藏状态栏。同样,大多数浏览器都强制显示它。
    • resizable(yes/no):允许禁用新窗口大小调整。不建议使用。
    • scrollbars(yes/no):允许禁用新窗口的滚动条。不建议使用。

7.1.3 操纵/关闭弹窗

操作弹窗

open 调用会返回对新窗口的引用。它可以用来操纵弹窗的属性,更改位置,甚至更多操作。

  • 同源策略:只有在窗口是同源的时,窗口才能自由访问彼此的内容(相同的协议://domain:port)。

关闭弹窗

关闭一个窗口:win.close()

检查一个窗口是否被关闭:win.closed。true 则表明已经被关闭

调整弹窗

  • win.moveBy(x,y):将窗口相对于当前位置向右移动 x 像素,向下移动 y 像素。
    • 允许负值(向上/向左移动)。
  • win.moveTo(x,y):将窗口移动到屏幕上的坐标 (x,y) 处。
  • win.resizeBy(width,height):根据给定的相对于当前大小的 width/height 调整窗口大小。允许负值。
  • win.resizeTo(width,height):将窗口调整为给定的大小。
  • window.onresize 事件:当窗口尺寸发生改变,触发该事件。

没有最小化/最大化。无法通过 JavaScript 最小化、最大化个窗口。

滚动窗口

  • win.scrollBy(x,y):相对于当前位置,将窗口向右滚动 x 像素,并向下滚动 y 像素。允许负值。
  • win.scrollTo(x,y):将窗口滚动到给定坐标 (x,y)
  • elem.scrollIntoView(top = true):滚动窗口,使 elem 显示在 elem.scrollIntoView(false) 的顶部(默认)或底部。
  • window.onscroll 事件:窗口发生滚动时触发。

聚焦/失焦

window.focus()window.blur() 方法可以使窗口获得或失去焦点。

在实际中它们被进行了严格地限制,因为在过去,恶意网站滥用这些方法。

7.2 跨窗口通信

“同源(Same Origin)”策略限制了窗口(window)和 frame 之间的相互访问。目的是为了提高安全性。

7.2.1 同源

如果两个 URL 具有相同的协议,域和端口,则称它们是“同源”的。

以下的几个 URL 都是同源的:

  • http://site.com
  • http://site.com/
  • http://site.com/my/page.html

但是下面这几个不是:

  • http://**www.**site.com(另一个域:www. 影响)
  • http://**site.org**(另一个域:.org 影响)
  • **https://**site.com(另一个协议:https
  • http://site.com:**8080**(另一个端口:8080

“同源”策略规定:

  • 如果我们有对另外一个窗口(例如,一个使用 window.open 创建的弹窗,或者一个窗口中的 iframe)的引用,并且该窗口是同源的,那么我们就具有对该窗口的全部访问权限。
  • 否则,如果该窗口不是同源的,那么我们就无法访问该窗口中的内容:变量,文档,任何东西。
    • 唯一的例外是 location:我们可以修改它(进而重定向用户)。但无法读取 location(因此,我们无法看到用户当前所处的位置,也就不会泄漏任何信息)。

7.2.2 iframe

一个 <iframe> 标签承载了一个单独的嵌入的窗口,它具有自己的 documentwindow

访问属性:

  • iframe.contentWindow :获取 <iframe> 中的 window。
  • iframe.contentDocument :获取 <iframe> 中的 document,
    • iframe.contentWindow.document 的简写形式。

当访问嵌入的窗口中的变量时,浏览器会检查 iframe 是否具有相同的源。如果不是,则拒绝访问,仅允许:

  • location 进行写入
  • 通过 iframe.contentWindow 获取对内部 window 的引用。

7.2.3 子域 - document.domain

根据定义,两个具有不同域的 URL 具有不同的源。

但是,如果窗口的二级域相同,例如 john.site.competer.site.comsite.com(它们共同的二级域是 site.com),可以使浏览器忽略差异,使它们作为“同源”的来对待,进行跨窗口通信。

为了做到这一点,每个这样的窗口都执行下面的代码:

document.domain = 'site.com';

7.2.4 集合 - window.frames

SomeWindow.frames:保存了该窗口下的全部 <iframe>window 对象

  • 通过索引获取:SomeWindow.frames[0] :文档中的第一个 iframe 的 window 对象。
  • 通过名称获取:SomeWindow.frames.iframeName :获取 name="iframeName" 的 window 对象。
<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>

<script>
alert(iframe.contentWindow == frames[0]); // true
alert(iframe.contentWindow == frames.win); // true
</script>

一个 iframe 内可能嵌套了其他的 iframe。相应的 window 对象会形成一个层次结构(hierarchy)。

可以通过以下方式获取:

  • SomeWindow.frames —— “子”窗口的集合(用于嵌套的 iframe)。
  • SomeWindow.parent —— 对“父”(外部)窗口的引用。
  • SomeWindow.top —— 对最顶级父窗口的引用。
SomeWindow.frames[0].parent === window; // true

7.2.5 sandbox - iframe 特性

sandbox 特性(attribute)允许在 <iframe> 中禁止某些行为,防止执行不被信任的代码。

  • 它通过将 iframe 视为非同源的,或者应用其他限制来实现 iframe 的“沙盒化”。

一个空的 "sandbox" 特性会施加最严格的限制。用一个以空格分隔的列表,列出要移除的限制。

<iframe sandbox="allow-forms allow-popups">
  • allow-same-origin:默认情况下,"sandbox" 会为 iframe 强制实施“不同来源”的策略。
    • 它使浏览器将 iframe 视为来自另一个源,即使其 src 指向的是同一个网站也是如此。
  • allow-top-navigation:允许 iframe 更改 parent.location
  • allow-forms:允许在 iframe 中提交表单。
  • allow-scripts:允许在 iframe 中运行脚本。
  • allow-popups:允许在 iframe 中使用 window.open 打开弹窗。
  • 查看 官方手册 获取更多内容。

7.2.6 跨窗口通信

postMessage 接口允许两个具有任何源的窗口之间进行通信。

因此,这是解决“同源”策略的方式之一。它允许来自于 john-smith.com 的窗口与来自于 gmail.com 的窗口进行通信,并交换信息,但前提是它们双方必须均同意并调用相应的 JavaScript 函数。这可以保护用户的安全。

postMessage

发送方必须调用窗口的 postMessage 方法。

  • 想把消息发送给 targetWin,就要调用: targetWin.postMessage(data, targetOrigin)

  • data:要发送的数据。

    • 可以是任何对象,数据会被 “结构化序列化算法(structured serialization algorithm)”进行克隆。
    • IE 浏览器只支持字符串,因此需要用 JSON.stringify 方法进行处理,以支持该浏览器。
  • targetOrigin:指定目标窗口的源,只有目标源窗口发送的信息,才能获得。

    • 这是一种安全措施。
    • 如果我们不希望做这个检查,可以将 targetOrigin 设置为 *

onmessage

为了接收消息,目标窗口应该在 message 事件上有一个处理程序。

  • postMessage 被调用时触发该事件,并且 targetOrigin 检查成功。

调用:window.addEventListener("message", function(event){ ... };

  • data:从 postMessage 传递来的数据。
  • origin:发送方的源,例如 http://site.com
  • source:对发送方窗口的引用。方便立即用 source.postMessage(...) 发送回信息。

要为 message 事件分配处理程序,我们应该使用 addEventListenerwindow.onmessage 不起作用。

8 网络请求

每次我们打开一个网页时,浏览器都会(使用 HTTP HTTPS协议)发送网络请求,请求 HTML 文档,也请求该文档依赖的图片、字体、脚本和样式表等等资源。浏览器除了能根据用户操作发送网络请求,也提供相关的 JavaScript API。

本章介绍 3 个网络 API:

  • 基于 Promise 的 fetch 方法。可以发送 HTTP 和 HTTPS 请求。 fetch() API 几乎满足所有 HTTP 用例。
    • fetch() API 取代了过时的 XMLHttpRequest API
  • SSE(Server-Send Event,服务器发送事件)。是为 HTTP “轮询” 技术提供的基于事件的一系列 API 接口。让 Web 服务器一直保持监听状态,随时向客户端发送数据。
  • WebSocket。是一个网络协议,不是基于 HTTP 但在设计时考虑了与 HTTP 交互。它定义了一个异步消息传递 API,即客户端和服务器可以通过与 TCP 网络套接口类似的方式相互发送和接收信息。

8.1 Fetch

JavaScript 可以将网络请求从用户页面端发送到服务器,并在需要时加载新信息到用户端。

例如,我们可以使用网络请求来:

  • 提交订单,
  • 加载用户信息,
  • 从服务器接收最新的更新,
  • ……等。

加载新信息不需要完整的重新加载页面。

对于来自 JavaScript 的网络请求,有一个总称术语 “AJAX”(Asynchronous JavaScript And XML 的简称)。

  • 我们不必使用 XML 技术:这个术语诞生于很久以前,所以 XML 虽然推出历史舞台,但名称未变。

基本语法:

let promise = fetch(url, [options])
  • url:要访问的 URL。
  • options:可选参数:method,header 等。

一个普通的 GET 请求,没有 option 参数,就会下载 url 的内容。

具体流程是这样的:

  1. 浏览器立即启动请求,并返回等待返回的 promise,里面保存着获取的结果。

  2. 获取响应的第一阶段:

    1. 服务器向用户端发送了响应头(response header),在一切都成功的情况下,客户端会收到该信息。
    2. 客户端的 fetch()得到一个已解决的 promise,这就是一个 Response 对象。
    3. 此时我们得到了一个 response header,通过调用一些方法,我们可以继续请求 response body
  3. 获取响应的第二阶段:

    1. 服务器向用户端发送了响应体(response body),在一切都成功的情况下,客户端会收到该信息。

我们可以进行:

  • 通过检查响应头,来判断 HTTP 状态以确定请求是否成功。
  • 如果 fetch 无法建立一个 HTTP 请求。如网络问题,请求网址不存在,则 promise 会 reject。
  • 如果 HTTP 状态异常,则不会 reject,而是会返回状态码,如 404,500。 response 属性查看:
    • response.status :HTTP 状态码,例如 200。
    • response.ok :布尔值,如果 HTTP 状态码为 200-299,则为 true
  1. 获取响应的第二阶段:

    1. 确定连接成功,开始获取响应体(response body

    Response 提供了多种基于 promise 的方法,来以不同的格式访问 body:

    • response.text():读取 response,并以文本形式返回 response,
    • response.json():将 response 解析为 JSON,
    • response.formData():以 FormData 对象( 这里解释 )的形式返回 response,
    • response.blob():以 Blob(具有类型的二进制数据)形式返回 response,
    • response.arrayBuffer():以 ArrayBuffer(低级别的二进制数据)形式返回 response,
    • response.bodyReadableStream 对象,它允许逐块读取 body。

举例,从 GitHub 获取最新 commits 的 JSON 对象:

  • 也就是说:发送一个 fetch 后,返回的第一个 promise 通常称之为 response
// 使用 Promise
fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
.then(response => response.json())
.then(commits => alert(commits[0].author.login));

// 使用 await
let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);
let commits = await response.json(); // 读取 response body,并将其解析为 JSON
alert(commits[0].author.login);
  • 我选择一种读取 body 的方法。
  • 如果已经使用了 response.text() 方法来获取 response。再用 response.json(),则不会生效,因为 body 内容已经被处理过了。

Response header 响应 (服务端->客户端)

response.headers 指向一个类 Map 的 header 对象。

  • 虽然不是 Map,但具有类似的方法,可以按名称 name 迭代 header:

    let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

    // 获取一个 header
    header2 = response.headers.get('Content-Type'));
    console.log(header2); // application/json; charset=utf-8

    // 迭代所有 header
    for (let [key, value] of response.headers) {
    console.log(`${key} = ${value}`);
    }

Request header 请求 (客户端->服务端)

在向服务端发送 fetch 时,可设置具体的 Request header,在 headers 可选参数中设置:

let response = fetch(protectedUrl, {
headers: {
Authentication: 'secret'
// ...
}
});

这里有非常多的属性,有些可以设置,有些不可以,具体属性不在此详赘了。

8.2 WebSocket

8.3 Server Sent Events

9 存储

使用浏览器 API,可以在用户计算机上本地存储数据。客户端存储的目的是让浏览器能够记住一些信息。

  • 比如:存储对网页设置的用户偏好,存储页面完成状态以便恢复上次离开时的情景等等。

客户端存储有两个要点:

  • 数据的访问权限:数据是按照来源隔离的。因此来自 A 站的的页面不能读取来自 B 站的页面存储的数据。
  • 数据的生命周期:
    • 可以临时存储(关闭页面就删除数据、时钟到期就删除数据)
    • 可以长期存储(存储在用户计算机硬盘中)

客户端存储分为三种主要的形式:

  • Web Storage:分为 localStoragesessionStorage 对象。是常用的存储方式。
  • Cookie:一种古老的存储方式。保存在 Cookie 中的数据也会随 HTTP 请求发送给服务器,只能存极少的数据
  • IndexedDB:一个支持索引的对象数据库。

9.1 localStoragesessionStorage

Window 对象的 localStoragesessionStorage 的属性,引用的是 Storage 对象。 Storage 对象与普通 JavaScript 对象非常相似,不同之处:

  • Storage 对象的属性值必须是字符串;
  • Storage 对象中存储的属性是持久化的。

可用的操作:

delete:删除 localStoragesessionStorage 的中属性;

for .. in:遍历 localStoragesessionStorage 的中属性;

localStorage.clear():删除 storage 对象的所有属性。

读写属性、删除属性:

  • getItem()

  • setItem()

  • deleteItem()

生命期和作用域

生命期:

  • localStorage:数据是永久性的,除非 Web 应用或用户通过浏览器(特定的界面)删除,否则数据会永远保存在用户设备上。

  • sessionStorage :数据的生命期与存储它的脚本所属的顶级窗口或浏览器标签页相同。窗口或标签页关闭后,sessionStorage 中存储的数据也都会被删除。

作用域:

  • localStorage:作用域为文档来源。文档来源由协议、域名、端口共同定义。所有同源文档都共享相同的 localstorage 数据。
  • sessionStorage :作用域为文档来源 + 窗口隔离。如果用户在两个浏览器标签页中打开了同一来源的文档,这两个标签页的 sessionStorage数据也是隔离的。

存储事件

window.onstorage 事件:存储在 localStorage 中的数据每当发生变化,在数据可见的 Window 对象上就会触发该事件。也就是说,如果在作用域内的所有 window 窗口(即使是不同的标签页),都可以监听到数据的变化。

localStoragestorage 事件可以作为一种广播机制,即浏览器向所有当前浏览同一网站的窗口发送信息。

cookie 是浏览器为特定网页或网站保存的少量命名数据。 cookie 是为服务器编程而设计的,在最底层上作为 HTTP 协议的扩展实现。cookie 数据会自动在浏览器与 Web 服务器之间传输,因此服务器端脚本可以读写存储在客户端的 cookie 值。

document.cookie 属性返回一个包含当前文档有关的所有 cookie的字符串。字符串是一个分号和空格分隔的 K/V 对。cookie 的值就是 Value。

  • 通常调用 split() 方法把整个字符串拆分成一个个 K/V 对。

生命期和作用域

cookie 有一个可选的属性,可以控制其生命期和作用域。

生命期:

  • 默认:生命期很短,只在浏览器会话期间存在,用户退出浏览器后,就会丢失。
  • 指定生命期:以秒为单位,时间到期后会删除数据。如果指定了时间长的生命期,浏览器就会把 cookie 存储在本地文件中,等时间到了再把它们删除。

作用域:为文档来源 + 文档路径。

  • 文档来源(domain 属性):同 localStorage 效果。
  • 文档路径(path 属性):默认情况下, cookie 的可访问权限为 该网页位于相同目录和子目录下的其他网页(即,兄弟网页、子网页、兄弟的子网页)。
    • 也可以指定 path 路径。这样来自同一服务器的任何网页,都可以访问一个 cookie。

安全性:secure

  • 是一个布尔值,默认情况下是不安全的。如果设置为安全的,则浏览器和服务器通过 HTTPS 或其他安全协议连接,才可以传输 cookie。

9.3 IndexedDB

IndexedDB 是一个简单的对象数据库。可以通过 JavaScript API 在用户计算机上持久存储 JavaScript 对象。

  • 作用域:文档来源。
  • 生命期:在计算机上持续存储。

IndexedDB 提供了原子保证,有完整的事务处理机制。

IndexedDB 数据库的名字必须在当前来源下唯一。存储的是对象,会使用结构化克隆算法序列化为一个对象存储。每个对象必须有一个键,可以用于排序和从存储中检索。键必须唯一。

  • JavaScript 字符串、数值、Date 对象都是有效的键。也可以自动为插入数据库中的每个对象生成唯一的键。

查找数据

  • 可以按对象的某个属性查找
  • 可以按索引查找
  • 可以按唯一的键查找

更新数据库

IndexedDB 是异步的,且该 API 是在 Promise 得到广泛支持之前定义的,因此这个 API 是基于事件而不是 Promise。